package correctness;

import static org.junit.Assert.*;

import java.util.HashMap;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;

import model.LogicType;
import model.editormodel.EditorModel;
import model.editormodel.Input;
import model.editormodel.Output;
import model.simulationmodel.LogicValue;
import model.simulationmodel.SimulationMessage;
import model.simulationmodel.SimulationModel;
import model.simulationmodel.SimulationMessage.SimulationMessageType;

import org.junit.Test;

/**
 * <b> Mit dieser Testklasse wird Folgendes getestet:</b> <br>
 * <br>
 * Es wird eine Schaltung erstellt, die zirkulieren soll. Danach wird getestet,
 * ob dies der Fall ist. Dies wird erkannt, indem am Ausgang der Lampe
 * <code>UNDEFINED</code> anliegt.
 */
public class OscillationCheckTest {

	private static boolean threadRunning;
	private static boolean oscillates;

	/**
	 * Testet, ob eine Schaltung zirkuliert.
	 */
	@Test
	public void oscillation() {

		threadRunning = false;
		oscillates = false;

		// Initialisieren einer neuen Schaltung
		EditorModel eModel = new EditorModel();

		// Erstellen einer Schaltung. Dazu werden ein OR-Gatter, ein
		// NOT-Gatter, ein Schalter und eine Lampe und hinzugefuegt
		eModel.addLogicElement(LogicType.OR, "OR", 766, 44);
		eModel.addLogicElement(LogicType.NOT, "NOT", 766, 44);
		eModel.addLogicElement(LogicType.LAMP, "LAMP", 766, 44);
		eModel.addLogicElement(LogicType.SWITCH, "SWITCH", 98, 45);

		// Um Verbindungen setzen zu koennen, werden die Inputs und Outputs der
		// einzelnen Bauteile benoetigt. Dazu werden hier die benoetigten Inputs
		// der einzelnen Bauteile gespeichert.
		Map<String, Input> orInputs = new HashMap<String, Input>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor().get("OR_1")
				.getInputs());
		Map<String, Input> notInputs = new HashMap<String, Input>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor()
				.get("NOT_2").getInputs());
		Map<String, Input> lampInputs = new HashMap<String, Input>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor()
				.get("LAMP_3").getInputs());

		// Hier werden die benoetigten Outputs der einzelnen Bauteile
		// gespeichert
		Map<String, Output> orOutputs = new HashMap<String, Output>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor().get("OR_1")
				.getOutputs());
		Map<String, Output> notOutputs = new HashMap<String, Output>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor()
				.get("NOT_2").getOutputs());
		Map<String, Output> switchOutputs = new HashMap<String, Output>(eModel
				.getCircuitModel().getUnmodifableElementsOnEditor()
				.get("SWITCH_4").getOutputs());

		// Der Ausgang des Schalters wird mit dem ersten Eingang des OR-Gatters
		// verbunden
		eModel.setConnection(switchOutputs.get(Output.STD_O),
				orInputs.get(Input.STD_A));

		// Der Ausgang des OR-Gatters wird mit dem Eingang des NOT-Gatters
		// verbunden
		eModel.setConnection(orOutputs.get(Output.STD_O),
				notInputs.get(Input.STD_A));

		// Der Ausgang des NOT-Gatters wird mit dem zweiten Eingang des
		// OR-Gatters verbunden
		eModel.setConnection(notOutputs.get(Output.STD_O),
				orInputs.get(Input.STD_B));

		// Der Ausgang des NOT-Gatters wird zusaetzlich mit dem Eingang der
		// Lampe verbunden
		eModel.setConnection(notOutputs.get(Output.STD_O),
				lampInputs.get(Input.STD_A));

		// Das SimulationModel-Objekt wird initialisiert
		SimulationModel sModel = new SimulationModel(eModel);
		sModel.updateSimulationElements();

		// Setzt den Wert des Schalters auf L0
		sModel.getSimulationElements()
				.get(sModel.getLogicElements().get("SWITCH_4"))
				.setInputValue(Input.STD_A, LogicValue.L0);

		// Fuegt einen Pseudo-Observer hinzu
		PseudoObserver pseudoObserver = new PseudoObserver();
		sModel.addObserver(pseudoObserver);

		// Startet die Simulation
		threadRunning = true;
		sModel.startSimulation();
		while (threadRunning) {
			synchronized (pseudoObserver) {
				try {
					pseudoObserver.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		// Speichert den logischen Wert am Ausgang der Lampe
		LogicValue lampValue = sModel.getSimulationElements()
				.get(sModel.getLogicElements().get("LAMP_3"))
				.getLogicValue(Output.STD_O);

		// Prueft, ob der logische Wert am Ausgang der Lampe UNDEFINED ist und
		// stoppt anschliessend die Simulation
		assertEquals("Der Wert am Ausgang der Lampe ist ungleich UNDEFINED.",
				LogicValue.UNDEFINED, lampValue);
		sModel.stopSimulation();

		// Da die Schaltung zirkuliert, werden die betroffenen Elemente an die
		// Observer weitergegeben
		sModel.oszillationFound(eModel.getCircuitModel()
				.getUnmodifableElementsOnEditor().get("NOT_2"));
		sModel.oszillationFound(eModel.getCircuitModel()
				.getUnmodifableElementsOnEditor().get("OR_1"));

		// Prueft, ob die Observer wirklich benachrichtigt wurden
		assertTrue("Die Schaltung zirkuliert nicht", oscillates);

	}

	/**
	 * Innere Klasse, die einen pseudoObserver zur Verfuegung stellt
	 */
	class PseudoObserver implements Observer {

		@Override
		public void update(Observable observable, Object data) {

			if (data instanceof SimulationMessage) {
				SimulationMessageType sType = ((SimulationMessage) data)
						.getMsgType();

				if (sType == SimulationMessageType.FIRST_CALC_COMPLETE) {
					synchronized (this) {
						threadRunning = false;
						this.notifyAll();
					}
				} else if (sType == SimulationMessageType.OSZILLATION_FOUND) {
					oscillates = true;
				}
			}
		}
	}
}